package com.orion.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * jwt格式令牌
 *  使用redis来存储
 *
 * @author Administrator
 * @date 2021/9/24
 */

@Configuration
@EnableAuthorizationServer
public class Oauth2RedisJwtAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    public static final Integer ACCESS_TOKEN_VALID_SECONDS = 7200;
    public static final Integer REFRESH_TOKEN_VALID_SECONDS = 7 * 24 * 3600;

    /**
     * 认证管理
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private CustomBasicTokenFilter customBasicTokenFilter;

    /**
     * redis工厂，springBoot2默认使用lettue
     */
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 密码加密器
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private DataSource dataSource;

    /**
     * user认证
     */
    @Resource(name = "authUserDetailService")
    private UserDetailsService authUserDetailService;


    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.passwordEncoder(passwordEncoder);
        // 开启/oauth/check_token验证端口认证权限访问
        security.checkTokenAccess("isAuthenticated()");
        // 开启/oauth/token_key验证端口无权限访问
        security.tokenKeyAccess("permitAll()");
        /*
         *
         * 主要是让/oauth/token支持client_id和client_secret做登陆认证
         * 如果开启了allowFormAuthenticationForClients，那么就在BasicAuthenticationFilter之前
         * 添加ClientCredentialsTokenEndpointFilter,使用ClientDetailsUserDetailsService来进行登陆认证
         *
         * 前后分离返回统一json不开启
         */
        //security.allowFormAuthenticationForClients();
        security.addTokenEndpointAuthenticationFilter(customBasicTokenFilter);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //MUST：密码模式下需设置一个AuthenticationManager对象,获取 UserDetails信息
        //末确认点.userDetailsService(userDetailsService)

        /*
          使用 /pig4cloud/login 覆盖 原有的/oauth/token，注意这里是覆盖一旦配置 原有路径将失效
          endpoints.pathMapping("/oauth/token","/pig4cloud/login");
        */

        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        endpoints.approvalStore(tokenApprovalStore());
        endpoints.authorizationCodeServices(redisAuthorizationCodeServices());
        /*
          这个是处理oauth2那些grant_type、invalid code 之类的异常返回，但是clientId、clientSecret这些错它不管的，
          要靠上面上面的security来配置security.authenticationEntryPoint()
         */
        endpoints.exceptionTranslator(new CustomWebResponseExceptionTranslator());
         /* 有了tokenServices就不设置了
        endpoints.authenticationManager(authenticationManager);

        endpoints.setClientDetailsService(jdbcClientDetailsService());
        endpoints.tokenStore(new JdbcTokenStore(dataSource));

        //token增强器，多加点信息在里面
        endpoints.tokenEnhancer(tokenEnhancerChain());
        */

        //tokenServices没有的话会自动创建一个默认的
        endpoints.tokenServices(customTokenService());
        //密码password模式必须加上
        endpoints.authenticationManager(authenticationManager);
        endpoints.userDetailsService(authUserDetailService);

        //endpoints.accessTokenConverter(jwtAccessTokenConverter());
    }

    @Bean
    public DefaultTokenServices customTokenService() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setAccessTokenValiditySeconds(ACCESS_TOKEN_VALID_SECONDS);
        tokenServices.setRefreshTokenValiditySeconds(REFRESH_TOKEN_VALID_SECONDS);
        tokenServices.setReuseRefreshToken(true);
        tokenServices.setClientDetailsService(jdbcClientDetailsService());
        tokenServices.setTokenEnhancer(tokenEnhancerChain());
        tokenServices.setTokenStore(jwtTokenStore());
        tokenServices.setAuthenticationManager(authenticationManager);
        return tokenServices;
    }

    /**
     * token增强器
     */
    @Bean
    public TokenEnhancerChain tokenEnhancerChain() {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = Arrays.asList(jwtAccessTokenConverter(),new CustomTokenEnhancer());
        tokenEnhancerChain.setTokenEnhancers(delegates);
        return tokenEnhancerChain;
    }

    /**
     * jwt token在转换器
     */
    private JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("orion");
        return jwtAccessTokenConverter;
    }

    /**
     * jwt token 存储
     *
     * @return
     */
    @Bean
    public TokenStore jwtTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    /**
     * clientId 和  client_secret 的身份校验
     *
     * @return
     */
    @Bean
    public ClientDetailsService jdbcClientDetailsService() {
        //从数据库中读取client_id,client_secret
        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 自定义一个授权码服务存储，使用redis来存储 授权码
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices redisAuthorizationCodeServices() {
        RedisTemplate<String, OAuth2Authentication> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.afterPropertiesSet();

        return new RandomValueAuthorizationCodeServices() {
            @Override
            protected void store(String code, OAuth2Authentication authentication) {
                //5分钟有效期
                redisTemplate.opsForValue().set(code, authentication, 5, TimeUnit.MINUTES);
            }

            @Override
            protected OAuth2Authentication remove(String code) {
                OAuth2Authentication auth2Authentication = redisTemplate.opsForValue().get(code);
                redisTemplate.delete(code);
                return auth2Authentication;
            }
        };
    }

    @Bean
    public ApprovalStore tokenApprovalStore() {
        return new TokenApprovalStore();
    }

}
